#include <stdarg.h>
#include <stdlib.h>
#include "org_oxygen_redscript_Engine__.h"
#include "org_oxygen_redscript_objects_RedCode.h"
#include "org_oxygen_redscript_objects_RedType.h"
#include "org_oxygen_redscript_objects_RedObject.h"

extern "C"
{
#include "rssb.h"
#include "redobject.h"
#include "redintobject.h"
#include "redmapobject.h"
#include "redboolobject.h"
#include "redfileobject.h"
#include "rednullobject.h"
#include "redarrayobject.h"
#include "redfloatobject.h"
#include "redsuperobject.h"
#include "redtupleobject.h"
#include "redmoduleobject.h"
#include "redstringobject.h"
#include "redcfunctionobject.h"
#include "redexceptionobject.h"

/* cannot #include "mem.h" here cause it's a C header and contains C++ but not C keywords */
RED_EXPORT void RedMem_Free(void *ptr);
RED_EXPORT void *RedMem_Alloc(size_t size);
RED_EXPORT void *RedMem_Realloc(void *ptr, size_t size);
}

#define CLASS_RED_CODE			"org/oxygen/redscript/objects/RedCode"
#define CLASS_RED_TYPE			"org/oxygen/redscript/objects/RedType"
#define CLASS_RED_OBJECT		"org/oxygen/redscript/objects/RedObject"

#define isSequence(o)			(RedArray_Exact(o) || RedTuple_Exact(o))
#define getSequenceLen(o)		(RedArray_Exact(o) ? RedArray_Length(o) : RedTuple_Length(o))
#define getSequenceItem(o, i)	(RedArray_Exact(o) ? RedArray_Items(o)[i] : RedTuple_Items(o)[i])

/************************** Runtime **************************/

static RedObject *consts;
static RedObject *modules;

static RedObject *builtin_exit(void)
{
	RedException_Throw(&RedExc_SystemExit);
	return NULL;
}

static RedObject *builtin_eval(RedObject *script)
{
	if (!RedString_Exact(script))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "Script must be string");
		return NULL;
	}

	RedObject *name = RedString_FromCString("__eval__");
	RedObject *code = RedModule_Compile("<string>", name, script, consts);

	if (!code)
	{
		Red_UNREF(name);
		return NULL;
	}

	RedObject *module = RedModule_Eval(code);

	Red_UNREF(code);
	Red_UNREF(name);
	return module;
}

static RedObject *builtin_hash(RedObject *self)
{
	return RedInt_FromInt((int64_t)RedObject_Hash(self));
}

static RedObject *builtin_float(RedObject *value)
{
	if (RedString_Exact(value))
		return RedFloat_FromString(value);

	RedException_ThrowFormat(&RedExc_TypeError, "Can only convert strings to float");
	return NULL;
}

static RedObject *builtin_import(RedObject *fname)
{
	if (!RedString_Exact(fname))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "Module name must be string");
		return NULL;
	}

	RedObject *module = RedMap_GetString(modules, fname);

	if (module)
	{
		Red_REF(module);
		return module;
	}

	module = RedModule_Import(RedString_Buffer(fname), consts);

	if (!module)
		return NULL;

	RedMap_PutString(modules, fname, module);
	return module;
}

static RedObject *builtin_int(RedObject *args)
{
	switch (RedTuple_Length(args))
	{
		case 1:
		{
			RedObject *value = RedTuple_Items(args)[0];

			if (!RedString_Exact(value))
			{
				RedException_ThrowFormat(&RedExc_TypeError, "Can only convert strings to int");
				return NULL;
			}

			return RedInt_FromString(value, 0);
		}

		case 2:
		{
			RedObject *value = RedTuple_Items(args)[0];
			RedObject *vbase = RedTuple_Items(args)[1];

			if (!RedInt_Exact(vbase))
			{
				RedException_ThrowFormat(&RedExc_TypeError, "Number base must be integers");
				return NULL;
			}

			if (!RedString_Exact(value))
			{
				RedException_ThrowFormat(&RedExc_TypeError, "Can only convert strings to int");
				return NULL;
			}

			return RedInt_FromString(value, (int)RedInt_Value(vbase));
		}

		default:
		{
			RedException_ThrowFormat(&RedExc_ArgumentError, "int() takes 1 ~ 2 arguments, but %d given", RedTuple_Length(args));
			return NULL;
		}
	}
}

static RedObject *builtin_print(RedObject *args)
{
	for (int i = 0; i < RedTuple_Length(args); i++)
		if (!RedObject_Print(RedTuple_Items(args)[i]))
			return NULL;
		else if (i < RedTuple_Length(args) - 1)
			printf(" ");

	fflush(stdout);
	Red_Return_Null();
}

static RedObject *builtin_println(RedObject *args)
{
	for (int i = 0; i < RedTuple_Length(args); i++)
		if (!RedObject_Print(RedTuple_Items(args)[i]))
			return NULL;
		else if (i < RedTuple_Length(args) - 1)
			printf(" ");

	printf("\n");
	fflush(stdout);
	Red_Return_Null();
}

static inline void registerConst(const char *key, RedObject *value)
{
	RedObject *name = RedString_FromCString(key);

	RedMap_PutString(consts, name, value);
	Red_UNREF(name);
}

static inline void registerObject(const char *key, RedObject *function)
{
	registerConst(key, function);
	Red_UNREF(function);
}

/************************** Utils **************************/

/* enclose RedObject into Java RedObject */
static jobject enclose(JNIEnv *env, RedObject *object, const char *className)
{
	jclass Base = env->FindClass(CLASS_RED_OBJECT);
	jclass Class = env->FindClass(className);
	jobject result = env->NewObject(Class, env->GetMethodID(Class, "<init>", "()V"));

	env->SetLongField(result, env->GetFieldID(Base, "objectPtr", "J"), (long)object);
	env->DeleteLocalRef(Base);
	env->DeleteLocalRef(Class);
	return result;
}

/* extract RedObject from Java RedObject */
static RedObject *extract(JNIEnv *env, jobject object)
{
	jclass Class = env->FindClass(CLASS_RED_OBJECT);
	RedObject *result = (RedObject *)env->GetLongField(object, env->GetFieldID(Class, "objectPtr", "J"));

	if (result)
	{
		env->DeleteLocalRef(Class);
		return result;
	}

	jclass Exception = env->FindClass("java/lang/RuntimeException");

	env->ThrowNew(Exception, "Object was already disposed");
	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(Exception);
	return nullptr;
}

/* throw RedExceptions into Java Runtime */
static void throwToJava(JNIEnv *env)
{
	RedObject *exc = RedException_Exception();

	if (!exc)
	{
		jclass Class = env->FindClass("java/lang/RuntimeException");

		env->ThrowNew(Class, "Exception occured but no exceptions set");
		env->DeleteLocalRef(Class);
	}
	else if (exc->ob_type == &RedExc_MemoryError)
	{
		jclass Class = env->FindClass("java/lang/OutOfMemoryError");

		env->ThrowNew(Class, "RedScript was ran out of memory");
		env->DeleteLocalRef(Class);
	}
	else
	{
		jclass Class = env->FindClass("java/lang/RuntimeException");
		RedObject *message = RedObject_Repr(exc);

		env->ThrowNew(Class, message ? RedString_Buffer(message) : "Exception occured when repr'ing");
		env->DeleteLocalRef(Class);
		Red_UNREF(message);
	}
}

/* identify the type of Java Objects and fetch the Type Class */
static ValueType resolveTypeClass(JNIEnv *env, jclass type, jclass &TypeClass)
{
	jclass Class = env->FindClass("java/lang/String");
	jobject Primitive = nullptr;
	ValueType valueType = ObjectValue;

	if (env->IsSameObject(type, Class))
	{
		valueType = StringValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	Class = env->FindClass("java/lang/Void");
	Primitive = env->GetStaticObjectField(Class, env->GetStaticFieldID(Class, "TYPE", "Ljava/lang/Class;"));

	if (env->IsSameObject(type, Class) || env->IsSameObject(type, Primitive))
	{
		valueType = VoidValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(Primitive);

	Class = env->FindClass("java/lang/Boolean");
	Primitive = env->GetStaticObjectField(Class, env->GetStaticFieldID(Class, "TYPE", "Ljava/lang/Class;"));

	if (env->IsSameObject(type, Class) || env->IsSameObject(type, Primitive))
	{
		valueType = BooleanValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(Primitive);

	Class = env->FindClass("java/lang/Long");
	Primitive = env->GetStaticObjectField(Class, env->GetStaticFieldID(Class, "TYPE", "Ljava/lang/Class;"));

	if (env->IsSameObject(type, Class) || env->IsSameObject(type, Primitive))
	{
		valueType = LongValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(Primitive);

	Class = env->FindClass("java/lang/Short");
	Primitive = env->GetStaticObjectField(Class, env->GetStaticFieldID(Class, "TYPE", "Ljava/lang/Class;"));

	if (env->IsSameObject(type, Class) || env->IsSameObject(type, Primitive))
	{
		valueType = ShortValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(Primitive);

	Class = env->FindClass("java/lang/Integer");
	Primitive = env->GetStaticObjectField(Class, env->GetStaticFieldID(Class, "TYPE", "Ljava/lang/Class;"));

	if (env->IsSameObject(type, Class) || env->IsSameObject(type, Primitive))
	{
		valueType = IntValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(Primitive);

	Class = env->FindClass("java/lang/Character");
	Primitive = env->GetStaticObjectField(Class, env->GetStaticFieldID(Class, "TYPE", "Ljava/lang/Class;"));

	if (env->IsSameObject(type, Class) || env->IsSameObject(type, Primitive))
	{
		valueType = CharValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(Primitive);

	Class = env->FindClass("java/lang/Float");
	Primitive = env->GetStaticObjectField(Class, env->GetStaticFieldID(Class, "TYPE", "Ljava/lang/Class;"));

	if (env->IsSameObject(type, Class) || env->IsSameObject(type, Primitive))
	{
		valueType = FloatValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(Primitive);

	Class = env->FindClass("java/lang/Double");
	Primitive = env->GetStaticObjectField(Class, env->GetStaticFieldID(Class, "TYPE", "Ljava/lang/Class;"));

	if (env->IsSameObject(type, Class) || env->IsSameObject(type, Primitive))
	{
		valueType = DoubleValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	Class = env->FindClass("java/lang/Class");

	if (env->CallBooleanMethod(type, env->GetMethodID(Class, "isArray", "()Z")))
	{
		jclass ComponentType = (jclass)env->CallObjectMethod(type, env->GetMethodID(Class, "getComponentType", "()Ljava/lang/Class;"));

		env->DeleteLocalRef(Class);
		Class = ComponentType;
		valueType = ArrayValue;
		goto cleanup;
	}

	env->DeleteLocalRef(Class);
	Class = env->FindClass("java/lang/Object");

cleanup:
	if (Primitive)
		env->DeleteLocalRef(Primitive);

	TypeClass = Class;
	return valueType;
}

/* check whether the has the same type as Java Class */
static bool isSameType(JNIEnv *env, jclass type, RedObject *object)
{
	bool isSame = true;
	jclass TypeClass = nullptr;

	switch (resolveTypeClass(env, type, TypeClass))
	{
		case IntValue:
		case CharValue:
		case LongValue:
		case ShortValue:
			isSame = RedInt_Exact(object);
			break;

		case FloatValue:
		case DoubleValue:
			isSame = RedFloat_Exact(object);
			break;

		case VoidValue:
			isSame = false;
			break;

		case ArrayValue:
		{
			if (!RedNull_IsNull(object))
			{
				isSame = isSequence(object);
				for (int i = 0; isSame && i < getSequenceLen(object); i++)
					isSame = isSameType(env, TypeClass, getSequenceItem(object, i));
			}

			break;
		}

		case StringValue:
			isSame = RedString_Exact(object);
			break;

		case ObjectValue:
		case BooleanValue:
			break;
	}

	env->DeleteLocalRef(TypeClass);
	return isSame;
}

/* get primitive classes for Numbers */
static jclass getPrimitiveClass(JNIEnv *env, const char *name)
{
	jclass Class = env->FindClass("java/lang/Class");
	jstring className = env->NewStringUTF(name);
	jobject Primitive = env->CallStaticObjectMethod(Class,
		env->GetStaticMethodID(Class, "getPrimitiveClass", "(Ljava/lang/String;)Ljava/lang/Class;"), className);

	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(className);
	return (jclass)Primitive;
}

/* convert Java Objects into specific RedObjects */
static RedObject *objectToRedObject(JNIEnv *env, jobject object, jclass type)
{
	jclass TypeClass = nullptr;
	RedObject *result = nullptr;

	if (!object)
		Red_Return_Null();

	switch (resolveTypeClass(env, type, TypeClass))
	{
		case IntValue:		result = RedInt_FromInt(env->GetIntField(object, env->GetFieldID(TypeClass, "value", "I")));		break;
		case CharValue:		result = RedInt_FromInt(env->GetCharField(object, env->GetFieldID(TypeClass, "value", "C")));		break;
		case LongValue:		result = RedInt_FromInt(env->GetLongField(object, env->GetFieldID(TypeClass, "value", "J")));		break;
		case ShortValue:	result = RedInt_FromInt(env->GetShortField(object, env->GetFieldID(TypeClass, "value", "S")));		break;
		case BooleanValue:	result = RedBool_FromBool(env->GetBooleanField(object, env->GetFieldID(TypeClass, "value", "Z")));	break;

		case FloatValue:	result = RedFloat_FromFloat(env->GetFloatField(object, env->GetFieldID(TypeClass, "value", "F")));	break;
		case DoubleValue:	result = RedFloat_FromFloat(env->GetDoubleField(object, env->GetFieldID(TypeClass, "value", "D")));	break;

		case VoidValue:
		{
			result = (Red_REF(Red_Null), Red_Null);
			break;
		}

		case ArrayValue:
		{
			if ((result = RedObject_Create(&ArrayType, nullptr)))
			{
				Array *array = (Array *)result;
				jclass NestedClass = nullptr;

				array->env = env;
				array->type = resolveTypeClass(env, TypeClass, NestedClass);
				array->Class = (jclass)env->NewGlobalRef(TypeClass);
				array->content = (jarray)env->NewGlobalRef(object);

				if (array->type == VoidValue)
					env->FatalError("VoidArray");

				env->DeleteLocalRef(NestedClass);
			}

			break;
		}

		case StringValue:
		{
			int len = env->GetStringLength((jstring)object);
			const char *str = env->GetStringUTFChars((jstring)object, nullptr);

			result = RedString_FromBufferAndSize(str, len);
			env->ReleaseStringUTFChars((jstring)object, str);
			break;
		}

		case ObjectValue:
		{
			if ((result = RedObject_Create(&ScalaObjectType, nullptr)))
			{
				((ScalaObject *)result)->env = env;
				((ScalaObject *)result)->object = env->NewGlobalRef(object);
			}

			break;
		}
	}

	env->DeleteLocalRef(TypeClass);
	return result;
}

/* convert RedObjects into specific Java Objects */
static jobject objectToJavaObject(JNIEnv *env, RedObject *object)
{
	if (RedNull_IsNull(object))
	{
		return nullptr;
	}
	else if (RedInt_Exact(object))
	{
		jclass Class = env->FindClass("java/lang/Long");
		jobject result = env->CallStaticObjectMethod(Class, env->GetStaticMethodID(Class, "valueOf", "(J)Ljava/lang/Long;"), RedInt_Value(object));

		env->DeleteLocalRef(Class);
		return result;
	}
	else if (RedFloat_Exact(object))
	{
		jclass Class = env->FindClass("java/lang/Double");
		jobject result = env->CallStaticObjectMethod(Class, env->GetStaticMethodID(Class, "valueOf", "(D)Ljava/lang/Double;"), RedFloat_Value(object));

		env->DeleteLocalRef(Class);
		return result;
	}
	else if (RedString_Exact(object))
	{
		return env->NewStringUTF(RedString_Buffer(object));
	}
	else if (object->ob_type == &RedType_Bool)
	{
		jclass Class = env->FindClass("java/lang/Boolean");
		jobject result = env->CallStaticObjectMethod(Class, env->GetStaticMethodID(Class, "valueOf", "(Z)Ljava/lang/Boolean;"), RedBool_IsTrue(object));

		env->DeleteLocalRef(Class);
		return result;
	}
	else if (object->ob_type == &ArrayType)
	{
		return ((Array *)object)->content;
	}
	else if (object->ob_type == &ScalaObjectType)
	{
		return ((ScalaObject *)object)->object;
	}
	else if (!isSequence(object))
	{
		return enclose(env, object, CLASS_RED_OBJECT);
	}
	else
	{
		jclass Class = env->FindClass("java/lang/Object");
		jobjectArray array = env->NewObjectArray(getSequenceLen(object), Class, nullptr);

		for (int i = 0; i < getSequenceLen(object); i++)
			env->SetObjectArrayElement(array, i, objectToJavaObject(env, getSequenceItem(object, i)));

		env->DeleteLocalRef(Class);
		return array;
	}
}

/* convert RedTuple/RedArray into Java Arrays */
static jarray sequenceToJavaArray(JNIEnv *env, RedObject *sequence, jclass type);

/* convert RedObjects into specific Java Values */
static ValueType objectToJavaValue(JNIEnv *env, jvalue *value, RedObject *object, jclass type)
{
	jclass TypeClass;
	ValueType result;

	switch ((result = resolveTypeClass(env, type, TypeClass)))
	{
		case IntValue:		value->i = (int)RedInt_Value(object);		break;
		case CharValue:		value->c = (char)RedInt_Value(object);		break;
		case LongValue:		value->j = (long)RedInt_Value(object);		break;
		case ShortValue:	value->s = (short)RedInt_Value(object);		break;
		case BooleanValue:	value->z = (bool)RedBool_IsTrue(object);	break;

		case FloatValue:	value->f = (float)RedFloat_Value(object);	break;
		case DoubleValue:	value->d = (double)RedFloat_Value(object);	break;

		case VoidValue:		env->FatalError("VoidValue");							break;
		case ArrayValue:	value->l = sequenceToJavaArray(env, object, TypeClass);	break;

		case StringValue:
		case ObjectValue:
		{
			value->l = objectToJavaObject(env, object);
			break;
		}
	}

	env->DeleteLocalRef(TypeClass);
	return result;
}

/* convert RedTuple/RedArray into Java Arrays */
static jarray sequenceToJavaArray(JNIEnv *env, RedObject *sequence, jclass type)
{
	if (RedNull_IsNull(sequence))
		return nullptr;

	jvalue temp;
	jarray array = nullptr;
	jclass TypeClass = nullptr;
	ValueType valueType = resolveTypeClass(env, type, TypeClass);

	switch (valueType)
	{
		case IntValue:		array = env->NewIntArray(getSequenceLen(sequence));		break;
		case CharValue:		array = env->NewCharArray(getSequenceLen(sequence));	break;
		case LongValue:		array = env->NewLongArray(getSequenceLen(sequence));	break;
		case ShortValue:	array = env->NewShortArray(getSequenceLen(sequence));	break;
		case BooleanValue:	array = env->NewBooleanArray(getSequenceLen(sequence));	break;

		case FloatValue:	array = env->NewFloatArray(getSequenceLen(sequence));	break;
		case DoubleValue:	array = env->NewDoubleArray(getSequenceLen(sequence));	break;

		case VoidValue:
		{
			env->FatalError("VoidArray");
			break;
		}

		case ArrayValue:
		case StringValue:
		case ObjectValue:
		{
			array = env->NewObjectArray(getSequenceLen(sequence), type, nullptr);
			break;
		}
	}

	for (int i = 0; i < getSequenceLen(sequence); i++)
	{
		/* nested conversion */
		objectToJavaValue(env, &temp, getSequenceItem(sequence, i), type);

		switch (valueType)
		{
			case IntValue:		env->SetIntArrayRegion((jintArray)array, i, 1, &(temp.i));			break;
			case CharValue:		env->SetCharArrayRegion((jcharArray)array, i, 1, &(temp.c));		break;
			case LongValue:		env->SetLongArrayRegion((jlongArray)array, i, 1, &(temp.j));		break;
			case ShortValue:	env->SetShortArrayRegion((jshortArray)array, i, 1, &(temp.s));		break;
			case BooleanValue:	env->SetBooleanArrayRegion((jbooleanArray)array, i, 1, &(temp.z));	break;

			case FloatValue:	env->SetFloatArrayRegion((jfloatArray)array, i, 1, &(temp.f));		break;
			case DoubleValue:	env->SetDoubleArrayRegion((jdoubleArray)array, i, 1, &(temp.d));	break;

			case VoidValue:
			{
				env->FatalError("VoidArray");
				break;
			}

			case ArrayValue:
			case StringValue:
			case ObjectValue:
			{
				env->SetObjectArrayElement((jobjectArray)array, i, temp.l);
				env->DeleteLocalRef(temp.l);
				break;
			}
		}
	}

	env->DeleteLocalRef(TypeClass);
	return array;
}

/* convert Array[Any] to RedTupleObject */
static RedObject *arrayToRedTuple(JNIEnv *env, jobject args)
{
	jclass Class = env->FindClass("scala/collection/SeqLike");
	jmethodID apply = env->GetMethodID(Class, "apply", "(I)Ljava/lang/Object;");
	jmethodID length = env->GetMethodID(Class, "length", "()I");

	int count = env->CallIntMethod(args, length);
	RedObject *result = RedTuple_FromLength(count);

	for (int i = 0; i < count; i++)
	{
		jobject item = env->CallObjectMethod(args, apply, i);
		jclass ItemClass = env->GetObjectClass(item);

		RedTuple_Items(result)[i] = objectToRedObject(env, item, ItemClass);
		env->DeleteLocalRef(item);
		env->DeleteLocalRef(Class);
	}

	env->DeleteLocalRef(Class);
	return result;
}

/* convert Java Strings into RedStringObjects */
static RedObject *stringToRedString(JNIEnv *env, jstring string)
{
	const char *str = env->GetStringUTFChars(string, nullptr);
	RedObject *result = RedString_FromCString(str);

	env->ReleaseStringUTFChars(string, str);
	return result;
}

/* append RedString to another RedString, then unref both */
static inline void appendAndUnref(RedObject *&string, RedObject *text)
{
	RedObject *newString = RedString_Append(string, text);

	Red_UNREF(text);
	Red_UNREF(string);
	string = newString;
}

/* append C-style string to RedString, then unref the old one */
static inline void appendAndUnref(RedObject *&string, const char *format, ...)
{
	va_list args;
	va_start(args, format);
	appendAndUnref(string, RedString_FromCFormatVa(format, args));
}

/* dump Java Exceptions to RedString */
static RedObject *dumpException(JNIEnv *env)
{
	jthrowable exc = env->ExceptionOccurred();

	jclass Class = env->FindClass("java/lang/Throwable");
	jclass Trace = env->FindClass("java/lang/StackTraceElement");

	jobject header = env->CallObjectMethod(exc, env->GetMethodID(Class, "toString", "()Ljava/lang/String;"));
	jobject traceback = env->CallObjectMethod(exc, env->GetMethodID(Class, "getStackTrace", "()[Ljava/lang/StackTraceElement;"));

	RedObject *result = stringToRedString(env, (jstring)header);
	appendAndUnref(result, "\n");

	for (int i = 0; i < env->GetArrayLength((jarray)traceback); i++)
	{
		jobject item = env->GetObjectArrayElement((jobjectArray)traceback, i);
		jobject trace = env->CallObjectMethod(item, env->GetMethodID(Trace, "toString", "()Ljava/lang/String;"));

		appendAndUnref(result, "\tat ");
		appendAndUnref(result, stringToRedString(env, (jstring)trace));
		appendAndUnref(result, "\n");

		env->DeleteLocalRef(item);
		env->DeleteLocalRef(trace);
	}

	env->DeleteLocalRef(Class);
	env->DeleteLocalRef(Trace);
	env->DeleteLocalRef(header);
	env->DeleteLocalRef(traceback);
	return result;
}

/* dump Java Arrays to RedString */
static RedObject *dumpArrayItems(JNIEnv *env, jarray array, ValueType type, jclass TypeClass)
{
	int count = env->GetArrayLength(array);
	RedObject *result = RedString_FromCString("[");

#define reprArrayItem(Type, type, format, ...)													\
{																								\
	j##type *elems = env->Get##Type##ArrayElements((j##type##Array)array, nullptr);				\
																								\
	for (int i = 0; i < count; i++)																\
		appendAndUnref(result, i == count - 1 ? format : format ", ", elems[i] __VA_ARGS__);	\
																								\
	env->Release##Type##ArrayElements((j##type##Array)array, elems, JNI_ABORT);					\
	break;																						\
}

	switch (type)
	{
		case IntValue:		reprArrayItem(Int, int, "%d")
		case CharValue:		reprArrayItem(Char, char, "%d")
		case LongValue:		reprArrayItem(Long, long, "%ld")
		case ShortValue:	reprArrayItem(Short, short, "%d")
		case FloatValue:	reprArrayItem(Float, float, "%.2f")
		case DoubleValue:	reprArrayItem(Double, double, "%.2lf")
		case BooleanValue:	reprArrayItem(Boolean, boolean, "%s", ? "true" : "false")

		case VoidValue:
		{
			Red_UNREF(result);
			env->FatalError("VoidArray");
			RedException_ThrowFormat(&RedExc_InternalError, "VoidArray");
			return nullptr;
		}

		case ArrayValue:
		{
			jclass Class;
			ValueType valueType = resolveTypeClass(env, TypeClass, Class);

			for (int i = 0; i < count; i++)
			{
				jobject item = env->GetObjectArrayElement((jobjectArray)array, i);

				appendAndUnref(result, dumpArrayItems(env, (jarray)item, valueType, Class));
				appendAndUnref(result, i == count - 1 ? "" : ", ");
				env->DeleteLocalRef(item);
			}

			env->DeleteLocalRef(Class);
			break;
		}

		case StringValue:
		case ObjectValue:
		{
			jclass Class = env->FindClass("java/lang/Object");
			jmethodID toString = env->GetMethodID(Class, "toString", "()Ljava/lang/String;");

			for (int i = 0; i < count; i++)
			{
				jobject item = env->GetObjectArrayElement((jobjectArray)array, i);
				jstring itemString = (jstring)env->CallObjectMethod(item, toString);

				appendAndUnref(result, stringToRedString(env, itemString));
				env->DeleteLocalRef(item);
				env->DeleteLocalRef(itemString);
			}

			env->DeleteLocalRef(Class);
			break;
		}
	}

#undef reprArrayItem

	appendAndUnref(result, "]");
	return result;

}

/************************** Engine **************************/

void array_release(RedObject *self)
{
	Array *array = (Array *)self;

	array->env->DeleteGlobalRef(array->Class);
	array->env->DeleteGlobalRef(array->content);
	Red_RELEASE();
}

RedObject *array_repr(RedObject *self)
{
	Array *array = (Array *)self;

	if (!RedObject_ReprEnter(self))
		return RedString_FromCString("[...]");

	RedObject *result = RedString_FromCFormat("<Array @ %p ", self);
	RedObject *repred = dumpArrayItems(array->env, array->content, array->type, array->Class);

	if (!repred)
	{
		Red_UNREF(result);
		RedObject_ReprLeave(self);
		return nullptr;
	}

	appendAndUnref(result, repred);
	appendAndUnref(result, ">");
	RedObject_ReprLeave(self);
	return result;
}

RedObject *array_delitem(RedObject *self, RedObject *key)
{
	RedException_ThrowFormat(&RedExc_RuntimeError, "Cannot delete items from Scala Arrays");
	return nullptr;
}

RedObject *array_getitem(RedObject *self, RedObject *key)
{
	if (!RedInt_Exact(key))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "Array index must be integer");
		return nullptr;
	}

	Array *array = (Array *)self;
	int64_t index = RedInt_Value(key);

	if (index < 0 || index >= array->env->GetArrayLength(array->content))
	{
		RedException_ThrowFormat(&RedExc_IndexError, "Array index out of bound: %ld", index);
		return nullptr;
	}

#define getArrayIndex(Type, type, Convert)															\
{																									\
	j##type *data = array->env->Get##Type##ArrayElements((j##type##Array)array->content, nullptr);	\
	RedObject *result = Red##Convert##_From##Convert(data[index]);									\
																									\
	array->env->Release##Type##ArrayElements((j##type##Array)array->content, data, JNI_ABORT);		\
	return result;																					\
}

	switch (array->type)
	{
		case IntValue:		getArrayIndex(Int, int, Int)
		case CharValue:		getArrayIndex(Char, char, Int)
		case LongValue:		getArrayIndex(Long, long, Int)
		case ShortValue:	getArrayIndex(Short, short, Int)
		case BooleanValue:	getArrayIndex(Boolean, boolean, Bool)

		case FloatValue:	getArrayIndex(Float, float, Float)
		case DoubleValue:	getArrayIndex(Double, double, Float)

		case VoidValue:
		{
			array->env->FatalError("VoidValue");
			RedException_ThrowFormat(&RedExc_InternalError, "VoidArray");
			return nullptr;
		}

		case ArrayValue:
		case StringValue:
		case ObjectValue:
		{
			jobject item = array->env->GetObjectArrayElement((jobjectArray)array->content, (int)index);
			jclass Compoment = array->env->GetObjectClass(item);
			RedObject *result = objectToRedObject(array->env, item, Compoment);

			array->env->DeleteLocalRef(item);
			array->env->DeleteLocalRef(Compoment);
			return result;
		}
	}

#undef getArrayIndex

	RedException_ThrowFormat(&RedExc_InternalError, "That's impossible !!!");
	return nullptr;
}

RedObject *array_setitem(RedObject *self, RedObject *key, RedObject *value)
{
	if (!RedInt_Exact(key))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "Array index must be integer");
		return nullptr;
	}

	Array *array = (Array *)self;
	int64_t index = RedInt_Value(key);

	if (index < 0 || index >= array->env->GetArrayLength(array->content))
	{
		RedException_ThrowFormat(&RedExc_IndexError, "Array index out of bound: %ld", index);
		return nullptr;
	}

#define setArrayIndex(Type, type, Checker)														\
{																								\
	if (!Red##Checker##_Exact(value))															\
	{																							\
		RedException_ThrowFormat(&RedExc_TypeError,												\
			"Cannot set '%s' object to '%s' array", RedObject_ClassName(value), #type);			\
		return nullptr;																			\
	}																							\
																								\
	j##type data = (j##type)Red##Checker##_Value(value);										\
	array->env->Set##Type##ArrayRegion((j##type##Array)array->content, (int)index, 1, &data);	\
	Red_Return_Null();																			\
}

	switch (array->type)
	{
		case IntValue:		setArrayIndex(Int, int, Int)
		case CharValue:		setArrayIndex(Char, char, Int)
		case LongValue:		setArrayIndex(Long, long, Int)
		case ShortValue:	setArrayIndex(Short, short, Int)

		case FloatValue:	setArrayIndex(Float, float, Float)
		case DoubleValue:	setArrayIndex(Double, double, Float)

		case VoidValue:
		{
			array->env->FatalError("VoidValue");
			RedException_ThrowFormat(&RedExc_RuntimeError, "VoidArray");
			return nullptr;
		}

		case ArrayValue:
		{
			if (!isSameType(array->env, array->Class, value))
			{
				RedException_ThrowFormat(&RedExc_TypeError,
					"Cannot set '%s' object to nested array", RedObject_ClassName(value));
				return nullptr;
			}

			jarray data = sequenceToJavaArray(array->env, value, array->Class);
			array->env->SetObjectArrayElement((jobjectArray)array->content, (int)index, data);
			Red_Return_Null();
			return nullptr;
		}

		case StringValue:
		{
			if (!RedString_Exact(value))
			{
				RedException_ThrowFormat(&RedExc_TypeError,
					"Cannot set '%s' object to 'string' array", RedObject_ClassName(value));
				return nullptr;
			}

			jstring data = array->env->NewStringUTF(RedString_Buffer(value));
			array->env->SetObjectArrayElement((jobjectArray)array->content, (int)index, data);
			Red_Return_Null();
		}

		case ObjectValue:
		{
			jobject data = objectToJavaObject(array->env, value);

			Red_REF(value);
			array->env->SetObjectArrayElement((jobjectArray)array->content, (int)index, data);
			Red_Return_Null();
		}

		case BooleanValue:
		{
			jboolean data = RedBool_IsTrue(value);
			array->env->SetBooleanArrayRegion((jbooleanArray)array->content, (int)index, 1, &data);
			Red_Return_Null();
		}
	}

#undef setArrayIndex

	RedException_ThrowFormat(&RedExc_InternalError, "That's impossible !!!");
	return nullptr;
}

void method_release(RedObject *self)
{
	Method *method = (Method *)self;

	for (int i = 0; i < method->count; i++)
		method->env->DeleteGlobalRef(method->overrides[i]);

	RedMem_Free(method->overrides);
	method->env->DeleteGlobalRef(method->self);
	Red_UNREF(method->name);
	Red_RELEASE();
}

RedObject *method_invoke(RedObject *self, RedObject *args)
{
	Method *method = (Method *)self;

	jclass *matchedTypes = (jclass *)RedMem_Alloc(sizeof(jclass) * RedTuple_Length(args));
	jobject matchedMethod = nullptr;

	jclass Method = method->env->FindClass("java/lang/reflect/Method");
	jfieldID field = method->env->GetFieldID(Method, "parameterTypes", "[Ljava/lang/Class;");

	for (int i = 0; i < method->count; i++)
	{
		bool isSame = true;
		jobject callable = method->overrides[i];
		jobjectArray types = (jobjectArray)method->env->GetObjectField(callable, field);

		if (method->env->GetArrayLength(types) == RedTuple_Length(args))
		{
			for (int j = 0; isSame && j < RedTuple_Length(args); j++)
			{
				jobject formal = method->env->GetObjectArrayElement(types, j);

				isSame = isSameType(method->env, (jclass)formal, RedTuple_Items(args)[j]);
				method->env->DeleteLocalRef(formal);
			}

			if (isSame)
			{
				for (int j = 0; j < RedTuple_Length(args); j++)
					matchedTypes[j] = (jclass)method->env->GetObjectArrayElement(types, j);

				method->env->DeleteLocalRef(types);
				matchedMethod = method->overrides[i];
				break;
			}
		}

		method->env->DeleteLocalRef(types);
	}

	if (!matchedMethod)
	{
		RedMem_Free(matchedTypes);
		method->env->DeleteLocalRef(Method);
		RedException_ThrowFormat(&RedExc_ArgumentError, "No matching methods");
		return nullptr;
	}

	jvalue *values = (jvalue *)RedMem_Alloc(sizeof(jvalue) * RedTuple_Length(args));

	for (int i = 0; i < RedTuple_Length(args); i++)
	{
		objectToJavaValue(method->env, &values[i], RedTuple_Items(args)[i], matchedTypes[i]);
		method->env->DeleteLocalRef(matchedTypes[i]);
	}

	jobject returnType = method->env->GetObjectField(matchedMethod, method->env->GetFieldID(Method, "returnType", "Ljava/lang/Class;"));
	jmethodID methodId = method->env->FromReflectedMethod(matchedMethod);

	jobject result = nullptr;
	RedObject *returnValue = nullptr;

#define matchClass(type, Getter, Convert)													\
{																							\
	jclass TypeClass = getPrimitiveClass(method->env, #type);								\
																							\
	if (method->env->IsSameObject(returnType, TypeClass))									\
	{																						\
		type result = method->env->Call##Getter##MethodA(method->self, methodId, values);	\
																							\
		if (!method->env->ExceptionCheck())													\
		{																					\
			returnValue = Convert(result);													\
		}																					\
		else																				\
		{																					\
			RedObject *object = dumpException(method->env);									\
			RedException_ThrowObject(&ScalaExceptionType, object);							\
			Red_UNREF(object);																\
		}																					\
																							\
		method->env->DeleteLocalRef(TypeClass);												\
		goto cleanup;																		\
	}																						\
																							\
	method->env->DeleteLocalRef(TypeClass);													\
}

	matchClass(long  , Long  , RedInt_FromInt);
	matchClass(long  , Int   , RedInt_FromInt);
	matchClass(char  , Char  , RedInt_FromInt);
	matchClass(short , Short , RedInt_FromInt);
	matchClass(float , Float , RedFloat_FromFloat);
	matchClass(double, Double, RedFloat_FromFloat);

#undef matchClass

	result = method->env->CallObjectMethodA(method->self, methodId, values);

	if (!method->env->ExceptionCheck())
	{
		returnValue = objectToRedObject(method->env, result, (jclass)returnType);
	}
	else
	{
		RedObject *object = dumpException(method->env);
		RedException_ThrowObject(&ScalaExceptionType, object);
		Red_UNREF(object);
	}

	if (returnType && returnValue != Red_Null)
		method->env->DeleteLocalRef(result);

cleanup:
	RedMem_Free(values);
	RedMem_Free(matchedTypes);
	method->env->DeleteLocalRef(Method);
	method->env->DeleteLocalRef(returnType);
	return returnValue;
}

uint32_t object_hash(RedObject *self)
{
	ScalaObject *object = (ScalaObject *)self;

	jclass Class = object->env->GetObjectClass(object->object);
	jmethodID method = object->env->GetMethodID(Class, "hashCode", "()I");

	object->env->DeleteLocalRef(Class);
	return (uint32_t)object->env->CallIntMethod(object->object, method);
}

void object_release(RedObject *self)
{
	ScalaObject *object = (ScalaObject *)self;
	object->env->DeleteGlobalRef(object->object);
	Red_RELEASE();
}

RedObject *object_eq(RedObject *self, RedObject *other)
{
	if (other->ob_type != &ScalaObjectType)
		Red_Return_False();

	ScalaObject *object = (ScalaObject *)self;
	ScalaObject *another = (ScalaObject *)other;
	return RedBool_FromBool(object->env->IsSameObject(object->object, another->object));
}

RedObject *object_str(RedObject *self)
{
	if (!RedObject_ReprEnter(self))
		return RedString_FromCString("...");

	ScalaObject *object = (ScalaObject *)self;

	jclass Class = object->env->GetObjectClass(object->object);
	jobject string = object->env->CallObjectMethod(object->object, object->env->GetMethodID(Class, "toString", "()Ljava/lang/String;"));
	RedObject *result = stringToRedString(object->env, (jstring)string);

	RedObject_ReprLeave(self);
	object->env->DeleteLocalRef(Class);
	object->env->DeleteLocalRef(string);
	return result;
}

RedObject *object_repr(RedObject *self)
{
	if (!RedObject_ReprEnter(self))
		return RedString_FromCString("...");

	ScalaObject *object = (ScalaObject *)self;

	jclass Class = object->env->GetObjectClass(object->object);
	jobject string = object->env->CallObjectMethod(object->object, object->env->GetMethodID(Class, "toString", "()Ljava/lang/String;"));
	RedObject *result = RedString_FromCFormat("<ScalaObject @ %p `", self);

	appendAndUnref(result, stringToRedString(object->env, (jstring)string));
	appendAndUnref(result, "`>");

	RedObject_ReprLeave(self);
	object->env->DeleteLocalRef(Class);
	object->env->DeleteLocalRef(string);
	return result;
}

RedObject *object_delattr(RedObject *self, RedObject *attr)
{
	RedException_ThrowFormat(&RedExc_RuntimeError, "Cannot delete attributes from Scala Objects");
	return nullptr;
}

RedObject *object_getattr(RedObject *self, RedObject *attr)
{
	ScalaObject *object = (ScalaObject *)self;

	jclass Class = object->env->FindClass("java/lang/Class");
	jclass Object = object->env->GetObjectClass(object->object);
	jclass JMethod = object->env->FindClass("java/lang/reflect/Method");

	jstring attrib = object->env->NewStringUTF(RedString_Buffer(attr));
	jobjectArray params = object->env->NewObjectArray(0, Class, nullptr);

	jobject valueGetter = object->env->CallObjectMethod(Object, object->env->GetMethodID(Class,
		"getMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;"), attrib, params);

	if (valueGetter)
	{
		object->env->DeleteLocalRef(Class);
		object->env->DeleteLocalRef(Object);
		object->env->DeleteLocalRef(attrib);

		jobject type = object->env->GetObjectField(valueGetter, object->env->GetFieldID(JMethod, "returnType", "Ljava/lang/Class;"));
		jobject value = object->env->CallObjectMethod(valueGetter, object->env->GetMethodID(JMethod,
				"invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"), object->object, params);

		object->env->DeleteLocalRef(params);
		object->env->DeleteLocalRef(JMethod);
		object->env->DeleteLocalRef(valueGetter);

		RedObject *result = objectToRedObject(object->env, value, (jclass)type);

		object->env->DeleteLocalRef(type);
		object->env->DeleteLocalRef(value);
		return result;
	}

	object->env->ExceptionClear();
	object->env->DeleteLocalRef(attrib);

	jobjectArray methods = (jobjectArray)object->env->CallObjectMethod(Object,
		object->env->GetMethodID(Class, "getMethods", "()[Ljava/lang/reflect/Method;"));

	int matchCount = 0;
	jobject *overrides = (jobject *)RedMem_Alloc(sizeof(jobject) * object->env->GetArrayLength(methods));

	for (int i = 0; i < object->env->GetArrayLength(methods); i++)
	{
		jobject item = object->env->GetObjectArrayElement(methods, i);
		jobject field = object->env->GetObjectField(item, object->env->GetFieldID(JMethod, "name", "Ljava/lang/String;"));
		const char *name = object->env->GetStringUTFChars((jstring)field, nullptr);

		if (!strcmp(name, RedString_Buffer(attr)))
			overrides[matchCount++] = object->env->NewGlobalRef(item);

		object->env->ReleaseStringUTFChars((jstring)field, name);
		object->env->DeleteLocalRef(field);
		object->env->DeleteLocalRef(item);
	}

	object->env->DeleteLocalRef(Class);
	object->env->DeleteLocalRef(Object);
	object->env->DeleteLocalRef(JMethod);
	object->env->DeleteLocalRef(methods);

	if (!matchCount)
	{
		RedMem_Free(overrides);
		RedException_ThrowFormat(&RedExc_AttributeError, "'%s' object doesn't have attribute '%s'", RedObject_ClassName(self), RedString_Buffer(attr));
		return nullptr;
	}

	Method *wrapper = (Method *)RedObject_Create(&MethodType, nullptr);

	Red_REF(attr);
	wrapper->env = object->env;
	wrapper->name = attr;
	wrapper->self = object->env->NewGlobalRef(object->object);
	wrapper->count = matchCount;
	wrapper->overrides = (jobject *)RedMem_Realloc(overrides, sizeof(jobject) * matchCount);
	return (RedObject *)wrapper;
}

RedObject *object_setattr(RedObject *self, RedObject *attr, RedObject *value)
{
	ScalaObject *object = (ScalaObject *)self;
	RedStringObject *name = (RedStringObject *)RedString_AppendCString(attr, "_$eq");

	jclass Class = object->env->FindClass("java/lang/Class");
	jclass Object = object->env->GetObjectClass(object->object);
	jclass JMethod = object->env->FindClass("java/lang/reflect/Method");
	jobjectArray methods = (jobjectArray)object->env->CallObjectMethod(Object,
		object->env->GetMethodID(Class, "getMethods", "()[Ljava/lang/reflect/Method;"));

	jfieldID funcName = object->env->GetFieldID(JMethod, "name", "Ljava/lang/String;");
	jfieldID retrType = object->env->GetFieldID(JMethod, "returnType", "Ljava/lang/Class;");
	jfieldID paramTypes = object->env->GetFieldID(JMethod, "parameterTypes", "[Ljava/lang/Class;");

	jclass Value = nullptr;
	jobject setter = nullptr;

	for (int i = 0; i < object->env->GetArrayLength(methods); i++)
	{
		jobject method = object->env->GetObjectArrayElement(methods, i);
		jstring methodName = (jstring)object->env->GetObjectField(method, funcName);
		jobjectArray params = (jobjectArray)object->env->GetObjectField(method, paramTypes);
		const char *nameString = object->env->GetStringUTFChars(methodName, nullptr);

		if (!strcmp(nameString, name->st_buffer) && object->env->GetArrayLength(params) == 1)
		{
			jclass Type = nullptr;
			jclass returnType = (jclass)object->env->GetObjectField(method, retrType);

			if (resolveTypeClass(object->env, returnType, Type) == VoidValue)
			{
				jclass paramType = (jclass)object->env->GetObjectArrayElement(params, 0);

				if (isSameType(object->env, paramType, value))
				{
					Value = paramType;
					setter = method;

					object->env->ReleaseStringUTFChars(methodName, nameString);
					object->env->DeleteLocalRef(Type);
					object->env->DeleteLocalRef(params);
					object->env->DeleteLocalRef(methodName);
					object->env->DeleteLocalRef(returnType);
					break;
				}

				object->env->DeleteLocalRef(paramType);
			}

			object->env->DeleteLocalRef(Type);
			object->env->DeleteLocalRef(returnType);
		}

		object->env->ReleaseStringUTFChars(methodName, nameString);
		object->env->DeleteLocalRef(method);
		object->env->DeleteLocalRef(params);
		object->env->DeleteLocalRef(methodName);
	}

	if (!setter)
	{
		Red_UNREF(name);
		object->env->DeleteLocalRef(Class);
		object->env->DeleteLocalRef(Object);
		object->env->DeleteLocalRef(JMethod);
		object->env->DeleteLocalRef(methods);
		RedException_ThrowFormat(&RedExc_AttributeError,
			"No setter found for attribute '%s' of type '%s'", RedString_Buffer(attr), RedObject_ClassName(value));
		return nullptr;
	}

	jvalue param;
	jmethodID mid = object->env->FromReflectedMethod(setter);

	objectToJavaValue(object->env, &param, value, Value);
	object->env->CallVoidMethodA(object->object, mid, &param);

	Red_UNREF(name);
	object->env->DeleteLocalRef(Class);
	object->env->DeleteLocalRef(Value);
	object->env->DeleteLocalRef(Object);
	object->env->DeleteLocalRef(setter);
	object->env->DeleteLocalRef(JMethod);
	object->env->DeleteLocalRef(methods);
	Red_Return_Null();
}

/*
 * Class:     org_oxygen_redscript_Engine__
 * Method:    init
 * Signature: ([Ljava/lang/String;)V
 */
JNIEXPORT jboolean JNICALL Java_org_oxygen_redscript_Engine_00024_init(JNIEnv *env, jobject self, jobjectArray args)
{
	int argc = env->GetArrayLength(args);
	char **argv = (char **)malloc(sizeof(char *) * argc);

	for (int i = 0; i < argc; i++)
	{
		jobject obj = env->GetObjectArrayElement(args, i);
		jsize size = env->GetStringUTFLength((jstring)obj);
		const char *str = env->GetStringUTFChars((jstring)obj, nullptr);

		argv[i] = (char *)malloc(sizeof(char) * (size + 1));
		argv[i][size] = 0;

		memcpy(argv[i], str, size);
		env->ReleaseStringUTFChars((jstring)obj, str);
		env->DeleteLocalRef(obj);
	}

	if (!Red_Init(argc, argv))
	{
		jclass Class = env->FindClass("java/lang/RuntimeException");

		env->ThrowNew(Class, "Failed to inintialize runtime");
		env->DeleteLocalRef(Class);
		return false;
	}

	if (!RedType_Prepare(&ArrayType) ||
		!RedType_Prepare(&MethodType) ||
		!RedType_Prepare(&ScalaObjectType) ||
		!RedType_Prepare(&ScalaExceptionType))
	{
		jclass Class = env->FindClass("java/lang/RuntimeException");

		Red_Cleanup();
		env->ThrowNew(Class, "Failed to prepare wrapper types");
		env->DeleteLocalRef(Class);
		return false;
	}

	consts = RedMap_CreateStringKey();
	modules = RedMap_CreateStringKey();

	if (!consts || !modules)
	{
		jclass Class = env->FindClass("java/lang/RuntimeException");

		Red_UNREF(consts);
		Red_UNREF(modules);
		Red_Cleanup();

		env->ThrowNew(Class, "Failed to create global maps");
		env->DeleteLocalRef(Class);
		return false;
	}

	RedType_Init(&ArrayType);
	RedType_Init(&MethodType);
	RedType_Init(&ScalaObjectType);
	RedType_Init(&ScalaExceptionType);

	/* built-in constants */
	registerConst("null"	, Red_Null);
	registerConst("true"	, Red_True);
	registerConst("false"	, Red_False);

	/* built-in types */
	registerConst("map"		, (RedObject *)&RedType_Map);
	registerConst("type"	, (RedObject *)&RedType_Type);
	registerConst("array"	, (RedObject *)&RedType_Array);
	registerConst("super"	, (RedObject *)&RedType_Super);
	registerConst("tuple"	, (RedObject *)&RedType_Tuple);
	registerConst("object"	, (RedObject *)&RedType_Object);

	/* built-in functions */
	registerObject("dir"		, RedCFunction_WrapUnary(RedObject_Dir));
	registerObject("len"		, RedCFunction_WrapUnary(RedObject_Len));
	registerObject("str"		, RedCFunction_WrapUnary(RedObject_Str));
	registerObject("repr"		, RedCFunction_WrapUnary(RedObject_Repr));
	registerObject("delattr"	, RedCFunction_WrapBinary(RedObject_DelAttr));
	registerObject("getattr"	, RedCFunction_WrapBinary(RedObject_GetAttr));
	registerObject("setattr"	, RedCFunction_WrapTernary(RedObject_SetAttr));

	registerObject("eval"		, RedCFunction_WrapUnary(builtin_eval));
	registerObject("hash"		, RedCFunction_WrapUnary(builtin_hash));
	registerObject("float"		, RedCFunction_WrapUnary(builtin_float));
	registerObject("import"		, RedCFunction_WrapUnary(builtin_import));
	registerObject("exit"		, RedCFunction_WrapNullary(builtin_exit));

	registerObject("int"		, RedCFunction_WrapVarFunc(builtin_int));
	registerObject("print"		, RedCFunction_WrapVarFunc(builtin_print));
	registerObject("println"	, RedCFunction_WrapVarFunc(builtin_println));

	/* built-in base exception */
	registerConst("Exception"				, (RedObject *)&RedExc_Exception);
	registerConst("ScalaException"			, (RedObject *)&ScalaExceptionType);

	/* built-in exception types */
	registerConst("IOError"					, (RedObject *)&RedExc_IOError);
	registerConst("OSError"					, (RedObject *)&RedExc_OSError);
	registerConst("NameError"				, (RedObject *)&RedExc_NameError);
	registerConst("TypeError"				, (RedObject *)&RedExc_TypeError);
	registerConst("IndexError"				, (RedObject *)&RedExc_IndexError);
	registerConst("ValueError"				, (RedObject *)&RedExc_ValueError);
	registerConst("FormatError"				, (RedObject *)&RedExc_FormatError);
	registerConst("ImportError"				, (RedObject *)&RedExc_ImportError);
	registerConst("MemoryError"				, (RedObject *)&RedExc_MemoryError);
	registerConst("CompileError"			, (RedObject *)&RedExc_CompileError);
	registerConst("RuntimeError"			, (RedObject *)&RedExc_RuntimeError);
	registerConst("ArgumentError"			, (RedObject *)&RedExc_ArgumentError);
	registerConst("InternalError"			, (RedObject *)&RedExc_InternalError);
	registerConst("AttributeError"			, (RedObject *)&RedExc_AttributeError);
	registerConst("FloatPointError"			, (RedObject *)&RedExc_FloatPointError);
	registerConst("UnhashableError"			, (RedObject *)&RedExc_UnhashableError);
	registerConst("NotCallableError"		, (RedObject *)&RedExc_NotCallableError);
	registerConst("NotPrintableError"		, (RedObject *)&RedExc_NotPrintableError);
	registerConst("RecursiveTooDepthError"	, (RedObject *)&RedExc_RecursionTooDepthError);
	return true;
}

/*
 * Class:     org_oxygen_redscript_Engine__
 * Method:    cleanup
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_oxygen_redscript_Engine_00024_cleanup(JNIEnv *env, jobject self)
{
	Red_UNREF(consts);
	Red_UNREF(modules);
	RedType_Cleanup(&ArrayType);
	RedType_Cleanup(&MethodType);
	RedType_Cleanup(&ScalaObjectType);
	RedType_Cleanup(&ScalaExceptionType);
	Red_Cleanup();

	for (int i = 0; i < Red_Argc; i++)
		free(Red_Argv[i]);

	free(Red_Argv);
}

/*
 * Class:     org_oxygen_redscript_Engine__
 * Method:    compile
 * Signature: (Ljava/lang/String;)Lorg/oxygen/redscript/objects/RedModule;
 */
JNIEXPORT jobject JNICALL Java_org_oxygen_redscript_Engine_00024_compile(JNIEnv *env, jobject self, jstring scripts)
{
	const char *script = env->GetStringUTFChars(scripts, nullptr);

	RedObject *code = RedString_FromCString(script);
	RedObject *name = RedString_FromCString("__rssb__");
	RedObject *result = RedModule_Compile("<string>", name, code, consts);

	Red_UNREF(code);
	Red_UNREF(name);
	env->ReleaseStringUTFChars(scripts, script);

	if (result)
		return enclose(env, result, CLASS_RED_CODE);

	throwToJava(env);
	return nullptr;
}

/*
 * Class:     org_oxygen_redscript_Engine__
 * Method:    addObject
 * Signature: (Ljava/lang/String;Ljava/lang/Object;)Lorg/oxygen/redscript/objects/RedObject;
 */
JNIEXPORT jobject JNICALL Java_org_oxygen_redscript_Engine_00024_addObject(JNIEnv *env, jobject self, jstring name, jobject object)
{
	jclass Class = env->GetObjectClass(object);
	const char *key = env->GetStringUTFChars(name, nullptr);
	RedObject *result = objectToRedObject(env, object, Class);

	registerConst(key, result);
	env->DeleteLocalRef(Class);
	env->ReleaseStringUTFChars(name, key);
	return enclose(env, result, CLASS_RED_OBJECT);
}

/************************** Code **************************/

/*
 * Class:     org_oxygen_redscript_objects_RedCode
 * Method:    eval
 * Signature: ()Lorg/oxygen/redscript/objects/RedObject;
 */
JNIEXPORT jobject JNICALL Java_org_oxygen_redscript_objects_RedCode_eval(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);

	if (!object)
		return nullptr;

	RedObject *result = RedModule_Eval(object);

	if (result)
		return enclose(env, result, CLASS_RED_OBJECT);

	throwToJava(env);
	return nullptr;
}

/************************** Type **************************/

/*
 * Class:     org_oxygen_redscript_objects_RedType
 * Method:    isSubclassOf
 * Signature: (Lorg/oxygen/redscript/objects/RedType;)Z
 */
JNIEXPORT jboolean JNICALL Java_org_oxygen_redscript_objects_RedType_isSubclassOf(JNIEnv *env, jobject self, jobject other)
{
	RedObject *type = extract(env, other);
	RedObject *object = extract(env, self);
	return type && object && RedType_IsSubclass((RedTypeObject *)object, (RedTypeObject *)type);
}

/************************** Object **************************/

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    str
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_org_oxygen_redscript_objects_RedObject_str(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);

	if (!object)
		return nullptr;

	RedObject *string = RedObject_Str(object);

	if (!string)
	{
		throwToJava(env);
		return nullptr;
	}

	if (!RedString_Exact(string))
	{
		jclass Class = env->FindClass("java/lang/RuntimeException");

		Red_UNREF(string);
		env->ThrowNew(Class, "The str() result of RedObject must be a string");
		env->DeleteLocalRef(Class);
		return nullptr;
	}

	jstring result = env->NewStringUTF(RedString_Buffer(string));
	Red_UNREF(string);
	return result;
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    repr
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_org_oxygen_redscript_objects_RedObject_repr(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);

	if (!object)
		return nullptr;

	RedObject *string = RedObject_Repr(object);

	if (!string)
	{
		throwToJava(env);
		return nullptr;
	}

	if (!RedString_Exact(string))
	{
		jclass Class = env->FindClass("java/lang/RuntimeException");

		Red_UNREF(string);
		env->ThrowNew(Class, "The repr() result of RedObject must be a string");
		env->DeleteLocalRef(Class);
		return nullptr;
	}

	jstring result = env->NewStringUTF(RedString_Buffer(string));
	Red_UNREF(string);
	return result;
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    length
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_org_oxygen_redscript_objects_RedObject_length(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);

	if (!object)
		return 0;

	RedObject *length = RedObject_Len(object);

	if (!length)
	{
		throwToJava(env);
		return 0;
	}

	if (!RedInt_Exact(length))
	{
		jclass Class = env->FindClass("java/lang/RuntimeException");

		Red_UNREF(length);
		env->ThrowNew(Class, "The len() result of RedObject must be a integer");
		env->DeleteLocalRef(Class);
		return 0;
	}

	jint result = (jint)RedInt_Value(length);
	Red_UNREF(length);
	return result;
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    fields
 * Signature: ()[Ljava/lang/String;
 */
JNIEXPORT jobjectArray JNICALL Java_org_oxygen_redscript_objects_RedObject_fields(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);

	if (!object)
		return nullptr;

	RedObject *fields = RedObject_Dir(object);

	if (!fields)
	{
		throwToJava(env);
		return nullptr;
	}

	if (!RedTuple_Exact(fields))
	{
		jclass Class = env->FindClass("java/lang/RuntimeException");

		Red_UNREF(fields);
		env->ThrowNew(Class, "The dir() result of RedObject must be a tuple of strings");
		env->DeleteLocalRef(Class);
		return nullptr;
	}

	jclass String = env->FindClass("java/lang/String");
	jobjectArray result = env->NewObjectArray(RedTuple_Length(fields), String, nullptr);

	for (int i = 0; i < RedTuple_Length(fields); i++)
	{
		RedObject *item = RedTuple_Items(fields)[i];

		if (!RedString_Exact(item))
		{
			jclass Class = env->FindClass("java/lang/RuntimeException");

			Red_UNREF(fields);
			env->ThrowNew(Class, "The dir() result of RedObject must be a tuple of strings");
			env->DeleteLocalRef(Class);
			env->DeleteLocalRef(String);
			env->DeleteLocalRef(result);
			return nullptr;
		}

		jobject value = env->NewStringUTF(RedString_Buffer(item));

		env->SetObjectArrayElement(result, i, value);
		env->DeleteLocalRef(value);
	}

	env->DeleteLocalRef(String);
	return result;
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    isNull
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_org_oxygen_redscript_objects_RedObject_isNull(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);
	return object && RedNull_IsNull(object);
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    isTrue
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_org_oxygen_redscript_objects_RedObject_isTrue(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);
	return object && RedBool_IsTrue(object);
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    isCallable
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_org_oxygen_redscript_objects_RedObject_isCallable(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);
	return object && RedObject_Callable(object);
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    isInstanceOf
 * Signature: (Lorg/oxygen/redscript/objects/RedType;)Z
 */
JNIEXPORT jboolean JNICALL Java_org_oxygen_redscript_objects_RedObject_isInstanceOf(JNIEnv *env, jobject self, jobject type)
{
	RedObject *rstype = extract(env, type);
	RedObject *object = extract(env, self);
	return rstype && object && RedType_IsInstance(object, (RedTypeObject *)rstype);
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    delIndex
 * Signature: (Ljava/lang/Object;)V
 */
JNIEXPORT void JNICALL Java_org_oxygen_redscript_objects_RedObject_delIndex(JNIEnv *env, jobject self, jobject key)
{
	RedObject *object = extract(env, self);

	if (object)
	{
		jclass Class = env->GetObjectClass(key);
		RedObject *keyobj = objectToRedObject(env, key, Class);
		RedObject *result = RedObject_DelItem(object, keyobj);

		if (!result)
			throwToJava(env);

		Red_UNREF(keyobj);
		Red_UNREF(result);
		env->DeleteLocalRef(Class);
	}
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    getIndex
 * Signature: (Ljava/lang/Object;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_oxygen_redscript_objects_RedObject_getIndex(JNIEnv *env, jobject self, jobject key)
{
	RedObject *object = extract(env, self);

	if (!object)
		return nullptr;

	jclass Class = env->GetObjectClass(key);
	RedObject *keyobj = objectToRedObject(env, key, Class);
	RedObject *result = RedObject_DelItem(object, keyobj);

	if (!result)
	{
		throwToJava(env);
		Red_UNREF(keyobj);
		env->DeleteLocalRef(Class);
		return nullptr;
	}

	jobject output = objectToJavaObject(env, result);

	Red_UNREF(keyobj);
	Red_UNREF(result);
	env->DeleteLocalRef(Class);
	return output;
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    setIndex
 * Signature: (Ljava/lang/Object;Ljava/lang/Object;)V
 */
JNIEXPORT void JNICALL Java_org_oxygen_redscript_objects_RedObject_setIndex(JNIEnv *env, jobject self, jobject key, jobject value)
{
	RedObject *object = extract(env, self);

	if (object)
	{
		jclass KeyClass = env->GetObjectClass(key);
		jclass ValueClass = env->GetObjectClass(key);
		RedObject *keyobj = objectToRedObject(env, key, KeyClass);
		RedObject *valobj = objectToRedObject(env, value, ValueClass);
		RedObject *result = RedObject_SetItem(object, keyobj, valobj);

		if (!result)
			throwToJava(env);

		Red_UNREF(keyobj);
		Red_UNREF(valobj);
		Red_UNREF(result);
		env->DeleteLocalRef(KeyClass);
		env->DeleteLocalRef(ValueClass);
	}
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    delAttrib
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_org_oxygen_redscript_objects_RedObject_delAttrib(JNIEnv *env, jobject self, jstring name)
{
	RedObject *object = extract(env, self);

	if (object)
	{
		RedObject *attr = stringToRedString(env, name);
		RedObject *result = RedObject_DelAttr(object, attr);

		if (!result)
			throwToJava(env);

		Red_UNREF(attr);
		Red_UNREF(result);
	}
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    getAttrib
 * Signature: (Ljava/lang/String;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_oxygen_redscript_objects_RedObject_getAttrib(JNIEnv *env, jobject self, jstring name)
{
	RedObject *object = extract(env, self);

	if (!object)
		return nullptr;

	RedObject *attr = stringToRedString(env, name);
	RedObject *result = RedObject_GetAttr(object, attr);

	if (!result)
	{
		Red_UNREF(attr);
		throwToJava(env);
		return nullptr;
	}

	jobject output = objectToJavaObject(env, result);

	Red_UNREF(attr);
	Red_UNREF(result);
	return output;
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    setAttrib
 * Signature: (Ljava/lang/String;Ljava/lang/Object;)V
 */
JNIEXPORT void JNICALL Java_org_oxygen_redscript_objects_RedObject_setAttrib(JNIEnv *env, jobject self, jstring name, jobject value)
{
	RedObject *object = extract(env, self);

	if (object)
	{
		jclass Class = env->GetObjectClass(value);
		RedObject *attr = stringToRedString(env, name);
		RedObject *valobj = objectToRedObject(env, value, Class);
		RedObject *result = RedObject_SetAttr(object, attr, valobj);

		if (!result)
			throwToJava(env);

		Red_UNREF(attr);
		Red_UNREF(valobj);
		Red_UNREF(result);
	}
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    apply
 * Signature: (Lscala/collection/Seq;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_oxygen_redscript_objects_RedObject_apply(JNIEnv *env, jobject self, jobject args)
{
	RedObject *object = extract(env, self);

	if (!object)
		return nullptr;

	RedObject *tuple = arrayToRedTuple(env, args);
	RedObject *result = RedObject_Invoke(object, tuple);

	if (!result)
	{
		Red_UNREF(tuple);
		throwToJava(env);
		return nullptr;
	}

	jobject output = objectToJavaObject(env, result);

	Red_UNREF(tuple);
	Red_UNREF(result);
	return output;
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    applyOn
 * Signature: (Ljava/lang/String;Lscala/collection/Seq;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_org_oxygen_redscript_objects_RedObject_applyOn(JNIEnv *env, jobject self, jstring name, jobject args)
{
	RedObject *object = extract(env, self);

	if (!object)
		return nullptr;

	RedObject *key = stringToRedString(env, name);
	RedObject *tuple = arrayToRedTuple(env, args);
	RedObject *result = RedObject_InvokeMethod(object, RedString_Buffer(key), tuple);

	if (!result)
	{
		Red_UNREF(key);
		Red_UNREF(tuple);
		throwToJava(env);
		return nullptr;
	}

	jobject output = objectToJavaObject(env, result);

	Red_UNREF(key);
	Red_UNREF(tuple);
	Red_UNREF(result);
	return output;
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    ref
 * Signature: ()Lorg/oxygen/redscript/objects/RedObject;
 */
JNIEXPORT jobject JNICALL Java_org_oxygen_redscript_objects_RedObject_ref(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);

	Red_REF(object);
	return object ? self : nullptr;
}

/*
 * Class:     org_oxygen_redscript_objects_RedObject
 * Method:    unref
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_oxygen_redscript_objects_RedObject_unref(JNIEnv *env, jobject self)
{
	RedObject *object = extract(env, self);

	if (object)
	{
		jclass Class = env->FindClass(CLASS_RED_OBJECT);

		env->SetLongField(self, env->GetFieldID(Class, "objectPtr", "J"), 0);
		env->DeleteLocalRef(Class);
		Red_UNREF(object);
	}
}
